package com.jfxia.arkles;

import javafx.data.pull.Event;
import javafx.data.pull.PullParser;

import java.io.InputStream;
import java.lang.Exception;
import java.lang.IllegalStateException;


/**
 * Parses a structured document (eg XML) firing events on the 
 * <code>EventNode</code> tree passed in as a whole document
 * (<code>rootEventNode</code>) or collection of triggers 
 * (<code>triggers</code>).
 */
public class Handler {
	/**
	 * Entire <code>EventNode</code> structure to use.  If you set this
	 * you cannot set <code>triggers</code>.
	 */
	public-init var rootEventNode:EventNode;
	/**
	 * Collection of <code>EventNode</code> triggers, tied to paths.  If 
	 * you set this you cannot set <code>rootEventNode</code>.
	 */
	public-init var triggers:Trigger[];
	/**
	 * Document input (eg and XML input stream).
	 */
	public-init var input:InputStream;

	var currentEventNode:EventNode = null;
	var currentEventPath:Event[];
	

	/**
	 * Begin the parser.
	 */
	public function parse() : Void {
		// Has some set both an plain event tree and a trigger collection?
		if(isInitialized(rootEventNode) and isInitialized(triggers)) {
			throw new IllegalStateException("You cannot set both 'rootEventNode' and 'triggers'");
		}
		
		def rootMode:Boolean = isInitialized(rootEventNode);
		
		// If the data is okay, begin the parser.
		def parser = PullParser {
			documentType: PullParser.XML;
			input: input;
			onEvent: if(rootMode) rootEventNodeHandler
				else triggerHandler;
		}
		parser.parse();
		parser.input.close();
	}

	/**
	 * Handle <code>PullParser</code> events when in trigger mode.  When
	 * not inside an EventNode, look for a trigger that matches the 
	 * current PullParser event location.
	 */
	function triggerHandler(ev:Event) : Void {
		// Are we not currently inside an event tree?
		if(currentEventNode==null) {
			// NO: Attempt to find an event tree from path
			if(ev.type == PullParser.START_ELEMENT) {
				// Attempt to match current path
				var match:Trigger = null;
				for(t in triggers) {
					if(t.test(currentEventPath)) { 
						match=t;
						break;
					}
				}
				// Did we find a match?
				if(match != null) {
					eventNodeHandler(ev , match.eventNode);
				} else {
					insert ev into currentEventPath;
				}
			} 
			else if(ev.type == PullParser.END_ELEMENT) {
				// Pop event from path
				delete currentEventPath[sizeof currentEventPath -1];
			}
		} else {
			// YES: handle PullParser event
			eventNodeHandler(ev,null);
		}
	}
	
	/**
	 * Handle <code>PullParser</code> events when in element mode.
	 */
	function rootEventNodeHandler(ev:Event) : Void {
		eventNodeHandler(ev,rootEventNode);
	}

	/**
	 * Low level function that handles events inside a
	 * <code>EventNode</code> tree.
	 */
	function eventNodeHandler(ev:Event,en:EventNode) : Void {
		if(ev.type == PullParser.START_ELEMENT) {
			// Are we entering a brand new EventNode tree?
			if(currentEventNode==null) {
				// Enter level: does the tag match the element?
				if(ev.qname.name == en.name) {
					// Set to current and call onStart()
					currentEventNode = en;
					if(currentEventNode.onStart != null) {
						currentEventNode.onStart(ev);
					}
				}
				else {
					// Tag name didn't match root EventNode
					throw new Exception(
						"Unmatched element and level {ev.level}: "
						"searching for {ev.qname.name}"
					);
				}
			} else {
				// Search current element for EventNode that matches event
				for(i in currentEventNode.content) {
					if(ev.qname.name == i.name) {
						// Found a match!  Set to current, call onStart()
						currentEventNode = i;
						if(currentEventNode.onStart != null) {
							currentEventNode.onStart(ev);
						}
					}
				}
			}
		}
		else if(ev.type == PullParser.TEXT) {
			if(currentEventNode.onText != null) {
				currentEventNode.onText(ev);
			}
		}
		else if(ev.type == PullParser.END_ELEMENT) {
			if(ev.qname.name == currentEventNode.name) {
				if(currentEventNode.onEnd!=null) {
					currentEventNode.onEnd(ev);
				}
				currentEventNode = currentEventNode.parent;
			}
		}
	}
}
